// Copyright (c) 2015, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

@Deprecated("Use the package_config.json based API")
library package_config.packages_file;

import "package:charcode/ascii.dart";

import "src/util.dart" show isValidPackageName;

/// Parses a `.packages` file into a map from package name to base URI.
///
/// The [source] is the byte content of a `.packages` file, assumed to be
/// UTF-8 encoded. In practice, all significant parts of the file must be ASCII,
/// so Latin-1 or Windows-1252 encoding will also work fine.
///
/// If the file content is available as a string, its [String.codeUnits] can
/// be used as the `source` argument of this function.
///
/// The [baseLocation] is used as a base URI to resolve all relative
/// URI references against.
/// If the content was read from a file, `baseLocation` should be the
/// location of that file.
///
/// If [allowDefaultPackage] is set to true, an entry with an empty package name
/// is accepted. This entry does not correspond to a package, but instead
/// represents a *default package* which non-package libraries may be considered
/// part of in some cases. The value of that entry must be a valid package name.
///
/// Returns a simple mapping from package name to package location.
/// If default package is allowed, the map maps the empty string to the default package's name.
Map<String, Uri> parse(List<int> source, Uri baseLocation,
    {bool allowDefaultPackage = false}) {
  var index = 0;
  var result = <String, Uri>{};
  while (index < source.length) {
    var isComment = false;
    var start = index;
    var separatorIndex = -1;
    var end = source.length;
    var char = source[index++];
    if (char == $cr || char == $lf) {
      continue;
    }
    if (char == $colon) {
      if (!allowDefaultPackage) {
        throw FormatException("Missing package name", source, index - 1);
      }
      separatorIndex = index - 1;
    }
    isComment = char == $hash;
    while (index < source.length) {
      char = source[index++];
      if (char == $colon && separatorIndex < 0) {
        separatorIndex = index - 1;
      } else if (char == $cr || char == $lf) {
        end = index - 1;
        break;
      }
    }
    if (isComment) continue;
    if (separatorIndex < 0) {
      throw FormatException("No ':' on line", source, index - 1);
    }
    var packageName = String.fromCharCodes(source, start, separatorIndex);
    if (packageName.isEmpty
        ? !allowDefaultPackage
        : !isValidPackageName(packageName)) {
      throw FormatException("Not a valid package name", packageName, 0);
    }
    var packageValue = String.fromCharCodes(source, separatorIndex + 1, end);
    Uri packageLocation;
    if (packageName.isEmpty) {
      if (!isValidPackageName(packageValue)) {
        throw FormatException(
            "Default package entry value is not a valid package name");
      }
      packageLocation = Uri(path: packageValue);
    } else {
      packageLocation = baseLocation.resolve(packageValue);
      if (!packageLocation.path.endsWith('/')) {
        packageLocation =
            packageLocation.replace(path: packageLocation.path + "/");
      }
    }
    if (result.containsKey(packageName)) {
      if (packageName.isEmpty) {
        throw FormatException(
            "More than one default package entry", source, start);
      }
      throw FormatException("Same package name occured twice", source, start);
    }
    result[packageName] = packageLocation;
  }
  return result;
}

/// Writes the mapping to a [StringSink].
///
/// If [comment] is provided, the output will contain this comment
/// with `# ` in front of each line.
/// Lines are defined as ending in line feed (`'\n'`). If the final
/// line of the comment doesn't end in a line feed, one will be added.
///
/// If [baseUri] is provided, package locations will be made relative
/// to the base URI, if possible, before writing.
///
/// If [allowDefaultPackage] is `true`, the [packageMapping] may contain an
/// empty string mapping to the _default package name_.
///
/// All the keys of [packageMapping] must be valid package names,
/// and the values must be URIs that do not have the `package:` scheme.
void write(StringSink output, Map<String, Uri> packageMapping,
    {Uri baseUri, String comment, bool allowDefaultPackage = false}) {
  ArgumentError.checkNotNull(allowDefaultPackage, 'allowDefaultPackage');

  if (baseUri != null && !baseUri.isAbsolute) {
    throw ArgumentError.value(baseUri, "baseUri", "Must be absolute");
  }

  if (comment != null) {
    var lines = comment.split('\n');
    if (lines.last.isEmpty) lines.removeLast();
    for (var commentLine in lines) {
      output.write('# ');
      output.writeln(commentLine);
    }
  } else {
    output.write("# generated by package:package_config at ");
    output.write(DateTime.now());
    output.writeln();
  }

  packageMapping.forEach((String packageName, Uri uri) {
    // If [packageName] is empty then [uri] is the _default package name_.
    if (allowDefaultPackage && packageName.isEmpty) {
      final defaultPackageName = uri.toString();
      if (!isValidPackageName(defaultPackageName)) {
        throw ArgumentError.value(
          defaultPackageName,
          'defaultPackageName',
          '"$defaultPackageName" is not a valid package name',
        );
      }
      output.write(':');
      output.write(defaultPackageName);
      output.writeln();
      return;
    }
    // Validate packageName.
    if (!isValidPackageName(packageName)) {
      throw ArgumentError('"$packageName" is not a valid package name');
    }
    if (uri.scheme == "package") {
      throw ArgumentError.value(
          "Package location must not be a package: URI", uri.toString());
    }
    output.write(packageName);
    output.write(':');
    // If baseUri provided, make uri relative.
    if (baseUri != null) {
      uri = _relativize(uri, baseUri);
    }
    if (!uri.path.endsWith('/')) {
      uri = uri.replace(path: uri.path + '/');
    }
    output.write(uri);
    output.writeln();
  });
}

/// Attempts to return a relative URI for [uri].
///
/// The result URI satisfies `baseUri.resolveUri(result) == uri`,
/// but may be relative.
/// The `baseUri` must be absolute.
Uri _relativize(Uri uri, Uri baseUri) {
  assert(baseUri.isAbsolute);
  if (uri.hasQuery || uri.hasFragment) {
    uri = Uri(
        scheme: uri.scheme,
        userInfo: uri.hasAuthority ? uri.userInfo : null,
        host: uri.hasAuthority ? uri.host : null,
        port: uri.hasAuthority ? uri.port : null,
        path: uri.path);
  }

  // Already relative. We assume the caller knows what they are doing.
  if (!uri.isAbsolute) return uri;

  if (baseUri.scheme != uri.scheme) {
    return uri;
  }

  // If authority differs, we could remove the scheme, but it's not worth it.
  if (uri.hasAuthority != baseUri.hasAuthority) return uri;
  if (uri.hasAuthority) {
    if (uri.userInfo != baseUri.userInfo ||
        uri.host.toLowerCase() != baseUri.host.toLowerCase() ||
        uri.port != baseUri.port) {
      return uri;
    }
  }

  baseUri = baseUri.normalizePath();
  var base = baseUri.pathSegments.toList();
  if (base.isNotEmpty) {
    base = List<String>.from(base)..removeLast();
  }
  uri = uri.normalizePath();
  var target = uri.pathSegments.toList();
  if (target.isNotEmpty && target.last.isEmpty) target.removeLast();
  var index = 0;
  while (index < base.length && index < target.length) {
    if (base[index] != target[index]) {
      break;
    }
    index++;
  }
  if (index == base.length) {
    if (index == target.length) {
      return Uri(path: "./");
    }
    return Uri(path: target.skip(index).join('/'));
  } else if (index > 0) {
    return Uri(
        path: '../' * (base.length - index) + target.skip(index).join('/'));
  } else {
    return uri;
  }
}
